// This file is part of Notepad4.
// See License.txt for details about distribution and modification.
//! Lexer for Markdown

#include <cassert>
#include <cstring>

#include <string>
#include <string_view>
#include <vector>

#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"

#include "WordList.h"
#include "LexAccessor.h"
#include "Accessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "StringUtils.h"
#include "LexerModule.h"
#include "LexerUtils.h"
#include "DocUtils.h"

using namespace Scintilla;
using namespace Lexilla;

namespace {

// CommonMark Spec https://spec.commonmark.org/
// https://spec.commonmark.org/dingus/
// GitHub Flavored Markdown Spec https://github.github.com/gfm/
// https://docs.github.com/en/get-started/writing-on-github
// GitLab Flavored Markdown https://docs.gitlab.com/ee/user/markdown.html
// https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md
// Pandoc’s Markdown https://pandoc.org/MANUAL.html#pandocs-markdown
// MultiMarkdown https://github.com/fletcher/MultiMarkdown-6
// https://fletcher.github.io/MultiMarkdown-6/
// PHP Markdown Extra https://michelf.ca/projects/php-markdown/extra/

enum class Markdown {
	GitHub,
	GitLab,
	Pandoc,
};

enum class HtmlTagState {
	None,
	Open,
	Value,
};

enum class HtmlTagType {
	Inline,
	Block,
	Both,
};

enum class AutoLink {
	None,
	Angle,
	Domain,
	Path,
};

enum class SeekStatus {
	None,
	Continue,
	Multiline
};

enum class HighlightResult {
	None,
	Finish,
	Continue,
	Invalid
};

enum {
	LineStateEmptyLine = 1 << 2,
	LineStateNestedStateLine = 1 << 3,
	LineStateListItemFirstLine = 1 << 4,
	LineStateBlockEndLine = 1 << 5,
	LineStateSetextFirstLine = 1 << 6,

	// minimum indentChild for unordered list or definition list as container
	// TODO: support block quote as container
	MinContainerIndentChild = 2,
	LineStateHtmlTagMask = 3,
	LineStateBlockMask = ((1 << 7) - 1) ^ LineStateNestedStateLine,
};

//KeywordIndex++Autogenerated -- start of section automatically generated
enum {
	KeywordIndex_HtmlBlockTag = 0,
	MaxKeywordSize = 16,
};
//KeywordIndex--Autogenerated -- end of section automatically generated

constexpr bool IsHeaderStyle(int state) noexcept {
	return state >= SCE_MARKDOWN_HEADER1 && state <= SCE_MARKDOWN_SETEXT_H2;
}

#define SCE_H_BLOCK_TAG			SCE_H_TAGEND	// style for block tag name
#define SCE_H_PROCESS_TEXT		SCE_H_SGML_BLOCK_DEFAULT
constexpr bool IsHtmlBlockStyle(int state) noexcept {
	return state == SCE_H_BLOCK_TAG
		|| state == SCE_H_COMMENT
		|| state == SCE_H_CDATA
		|| state == SCE_H_SGML_COMMAND
		|| state == SCE_H_SGML_DEFAULT
		|| state == SCE_H_PROCESS_TEXT
		|| state == SCE_H_QUESTION;
}

constexpr bool IsBlockStyle(int state) noexcept {
	return (state >= SCE_MARKDOWN_HEADER1 && state < SCE_MARKDOWN_EM_ASTERISK)
		|| IsHtmlBlockStyle(state);
}

constexpr bool IsRawTextStyle(int state) noexcept {
	return (state >= SCE_MARKDOWN_INDENTED_BLOCK && state <= SCE_MARKDOWN_DISPLAY_MATH)
		|| (state >= SCE_MARKDOWN_CODE_SPAN && state <= SCE_MARKDOWN_INLINE_MATH);
}

constexpr bool StyleNeedsBacktrack(int state) noexcept {
	return state == SCE_MARKDOWN_SETEXT_H1 || state == SCE_MARKDOWN_SETEXT_H2;
}

constexpr int GetIndentCount(uint32_t lineState) noexcept {
	return (lineState >> 16) & 0xff;
}

constexpr int GetIndentChild(uint32_t lineState) noexcept {
	return lineState >> 24;
}

inline uint8_t GetCharAfterSpace(LexAccessor &styler, Sci_PositionU &startPos, int count) noexcept {
	Sci_PositionU pos = startPos;
	uint8_t ch = styler.SafeGetCharAt(pos);
	while (ch == ' ' && count != 0) {
		--count;
		ch = styler[++pos];
	}
	startPos = pos;
	return ch;
}

inline uint8_t GetCharAfterDelimiter(LexAccessor &styler, Sci_PositionU &startPos, int delimiter) noexcept {
	Sci_PositionU pos = startPos;
	uint8_t ch;
	do {
		ch = styler[++pos];
	} while (ch == delimiter);
	startPos = pos;
	return ch;
}

// 5.2 List items
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-ol-element
// https://pandoc.org/MANUAL.html#ordered-lists
enum class OrderedListType {
	None = 0,
	Decimal,
	LowerRoman,
	UpperRoman,
	LowerAlpha,
	UpperAlpha,
	NumberSign, // Pandoc #.
	Parenthesis = 8,
};

constexpr bool IsStandardList(OrderedListType listType, Markdown markdown) noexcept {
	return listType == OrderedListType::Decimal || (listType != OrderedListType::None && markdown == Markdown::Pandoc);
}

constexpr bool IsUpperRoman(int ch) noexcept {
	return AnyOf(ch, 'I', 'V', 'X', 'L', 'C', 'D', 'M');
}

constexpr bool IsLowerRoman(int ch) noexcept {
	return AnyOf(ch, 'i', 'v', 'x', 'l', 'c', 'd', 'm');
}

// 6.2 Emphasis and strong emphasis
// 6.5 Strikethrough (extension), handled similar to `**` except no nesting
struct DelimiterRun {
	CharacterClass ccPrev = CharacterClass::space;
	CharacterClass ccNext = CharacterClass::space;

	constexpr bool IsLeftFlanking() const noexcept {
		// (1) not followed by Unicode whitespace
		if (ccNext >= CharacterClass::punctuation) {
			// (2b) followed by a Unicode punctuation character
			if (ccNext == CharacterClass::punctuation) {
				// and preceded by Unicode whitespace or a Unicode punctuation character
				return ccPrev <= CharacterClass::punctuation;
			}
			// (2a) not followed by a Unicode punctuation character,
			return true;
		}
		return false;
	}
	constexpr bool IsRightFlanking() const noexcept {
		// (1) not preceded by Unicode whitespace
		if (ccPrev >= CharacterClass::punctuation) {
			// (2b) preceded by a Unicode punctuation character
			if (ccPrev == CharacterClass::punctuation) {
				// and followed by Unicode whitespace or a Unicode punctuation character
				return ccNext <= CharacterClass::punctuation;
			}
			// (2a) not preceded by a Unicode punctuation character
			return true;
		}
		return false;
	}
	constexpr bool CanOpen(int delimiter) const noexcept {
		// is part of a left-flanking delimiter run
		if (IsLeftFlanking()) {
			if (delimiter != '_') {
				return true; // `*`, `**`, `~~`
			}
			// (a) not part of a right-flanking delimiter run or
			// (b) part of a right-flanking delimiter run preceded by a Unicode punctuation character.
			if (ccPrev == CharacterClass::punctuation || !IsRightFlanking()) {
				return true;
			}
		}
		return false;
	}
	constexpr bool CanClose(int delimiter) const noexcept {
		// is part of a right-flanking delimiter run
		if (IsRightFlanking()) {
			if (delimiter != '_') {
				return true; // `*`, `**`, `~~`
			}
			// (a) not part of a left-flanking delimiter run or
			// (b) part of a left-flanking delimiter run followed by a Unicode punctuation character.
			if (ccNext == CharacterClass::punctuation || !IsLeftFlanking()) {
				return true;
			}
		}
		return false;
	}
};

struct MarkdownLexer {
	StyleContext sc;
	std::vector<int> nestedState;
	std::vector<Sci_PositionU> backPos;

	HtmlTagState tagState = HtmlTagState::None; // html tag, link title
	int indentParent = 0; // parent container's indentChild
	int delimiterCount = 0; // code fence
	int bracketCount = 0; // link text
	int parenCount = 0; // link	destination, link title
	int periodCount = 0; // autoLink domain and path
	AutoLink autoLink = AutoLink::None;
	const Markdown markdown;
	const WordList &blockTagList;

	MarkdownLexer(Sci_PositionU startPos, Sci_PositionU lengthDoc, int initStyle, LexerWordList keywordLists, Accessor &styler):
		sc(startPos, lengthDoc, initStyle, styler),
		markdown{static_cast<Markdown>(styler.GetPropertyInt("lexer.lang"))},
		blockTagList{keywordLists[KeywordIndex_HtmlBlockTag]} {}

	void SaveOuterStyle(int style) {
		nestedState.push_back(style);
	}
	int TakeOuterStyle() {
		return TakeAndPop(nestedState);
	}
	int TryTakeOuterStyle() {
		return TryTakeAndPop(nestedState);
	}
	void DropOuterStyle() {
		nestedState.pop_back();
	}

	void SaveOuterStart(Sci_PositionU startPos) {
		backPos.push_back(startPos);
	}
	Sci_PositionU TakeOuterStart() {
		return TakeAndPop(backPos);
	}
	void DropOuterStart() {
		backPos.pop_back();
	}

	bool IsParagraphEnd(Sci_PositionU pos, uint32_t lineState) const noexcept;
	bool OnHeaderLine() const noexcept {
		return !nestedState.empty() && IsHeaderStyle(nestedState.front());
	}
	bool IsMultilineEnd(uint32_t lineState) const noexcept {
		return OnHeaderLine() || IsParagraphEnd(sc.lineStartNext, lineState);
	}

	int HighlightBlockText(uint32_t lineState);
	void HighlightInlineText();

	HtmlTagType CheckHtmlBlockTag(Sci_PositionU pos, Sci_PositionU endPos, int chNext, bool paragraph) const noexcept;
	bool HandleHtmlTag(HtmlTagType tagType);

	bool CheckDefinitionList(Sci_PositionU startPos, uint32_t lineState) const noexcept;
	int GetListChildIndentCount(int indentCurrent) const noexcept;

	int UpdateParentIndentCount(int indentCurrent) noexcept;
	uint32_t HighlightIndentedText(uint32_t lineState, int indentCount);
	bool IsIndentedBlockEnd() const noexcept;

	int GetCurrentDelimiterRun(DelimiterRun &delimiterRun, bool ignoreCurrent = false) const noexcept;
	SeekStatus HighlightEmphasis(uint32_t lineState);
	SeekStatus HighlightCodeSpan(uint32_t lineState);

	SeekStatus HighlightLinkText(uint32_t lineState);
	SeekStatus HighlightLinkDestination(uint32_t lineState);

	bool DetectAutoLink();
	bool HighlightAutoLink();

	void HighlightDelimiterRow();
	bool HighlightCriticMarkup();
};

// 4.1 Thematic breaks
bool CheckThematicBreak(LexAccessor &styler, Sci_PositionU pos, int delimiter) noexcept {
	int count = 1;
	while (true) {
		const uint8_t ch = styler[++pos];
		if (ch == delimiter) {
			++count;
		} else if (!IsSpaceOrTab(ch)) {
			if (IsEOLChar(ch)) {
				break;
			}
			return false;
		}
	}
	return count >= 3;
}

// 4.2 ATX headings
int CheckATXHeading(LexAccessor &styler, Sci_PositionU pos) noexcept {
	int level = 1;
	while (true) {
		const uint8_t ch = styler[++pos];
		if (ch == '#') {
			++level;
		} else {
			if (level <= 6 && IsASpace(ch)) {
				return level;
			}
			break;
		}
	}
	return 0;
}

// 4.3 Setext headings
int CheckSetextHeading(LexAccessor &styler, Sci_PositionU pos) noexcept {
	const uint8_t marker = GetCharAfterSpace(styler, pos, 3);
	if (marker == '=' || marker == '-') {
		uint8_t ch;
		do {
			ch = styler[++pos];
		} while (ch == marker);
		while (IsSpaceOrTab(ch)) {
			ch = styler[++pos];
		}
		if (IsEOLChar(ch)) {
			return (marker == '=') ? 1 : 2;
		}
	}
	return 0;
}

int GetBlockStyle(LexAccessor &styler, Sci_Line line, uint32_t lineState) noexcept {
	Sci_PositionU pos = styler.LineStart(line);
	const int indentCount = GetIndentCount(lineState);
	if (indentCount < 4) {
		pos += indentCount;
	} else {
		while (IsSpaceOrTab(styler[pos])) {
			++pos;
		}
	}
	int style = styler.BufferStyleAt(pos);
	if (style == SCE_H_TAG) {
		// <script> </script>
		style = styler.BufferStyleAt(pos + 1);
		if (style == SCE_H_TAG) {
			style = styler.BufferStyleAt(pos + 2);
		}
		if (style != SCE_H_BLOCK_TAG) {
			// <a href=""> </a>
			style = SCE_H_TAG;
		}
	}
	return style;
}

// 4.10 Tables (extension)
// https://pandoc.org/MANUAL.html#tables
void MarkdownLexer::HighlightDelimiterRow() {
	bool pipe = false;
	bool minus = false;
	bool plus = false;
	bool equal = false;
	Sci_PositionU pos = sc.currentPos;
	while (pos < sc.lineStartNext) {
		const uint8_t ch = sc.styler[pos++];
		switch (ch) {
		case '|':
			pipe = true;
			break;
		case ':':
			break;
		case '-':
			minus = true;
			break;
		case '+':
			plus = true;
			break;
		case '=':
			equal = true;
			break;
		default:
			if (!IsASpace(ch)) {
				if (sc.ch == ':' && (sc.chNext == ':' || IsSpaceOrTab(sc.chNext))) {
					// Pandoc fenced divs
					// Pandoc, MultiMarkdown, PHP Markdown Extra definition list
					SaveOuterStyle(SCE_MARKDOWN_DEFAULT);
					sc.SetState(SCE_MARKDOWN_DELIMITER);
				}
				return;
			}
			break;
		}
	}

	if (markdown == Markdown::Pandoc) {
		// pipe table or grid table
		pipe = (pipe | plus) & (minus | equal);
	} else {
		pipe &= minus;
	}
	if (pipe) {
		int style = SCE_MARKDOWN_DELIMITER_ROW;
		if (sc.ch == '|' || sc.ch == '+') {
			SaveOuterStyle(style);
			style = SCE_MARKDOWN_DELIMITER;
		}
		sc.SetState(style);
	}
}

OrderedListType CheckOrderedList(LexAccessor &styler, Sci_PositionU pos, int current, int chNext) noexcept {
	if (current == '#') { // Pandoc
		if (chNext == '.') {
			const uint8_t after = styler[pos + 2];
			if (IsASpace(after)) {
				return OrderedListType::NumberSign;
			}
		}
		return OrderedListType::None;
	}

	int marker = current;
	if (marker == '(') { // Pandoc
		marker = chNext;
		++pos;
		chNext = static_cast<uint8_t>(styler[pos + 1]);
	}

	// only support (1) single uppercase or lowercase letter.
	// (2) less than 10 decimal numbers or roman numerals.
	OrderedListType type;
	int count = 1;

	if (IsADigit(marker)) {
		type = OrderedListType::Decimal;
		if (IsADigit(chNext)) {
			++count;
		}
	} else if (IsLowerCase(marker)) {
		type = OrderedListType::LowerAlpha;
		if (IsLowerRoman(marker) && IsLowerRoman(chNext)) {
			type = OrderedListType::LowerRoman;
			++count;
		}
	} else if (IsUpperCase(marker)) {
		type = OrderedListType::UpperAlpha;
		if (IsUpperRoman(marker) && IsUpperRoman(chNext)) {
			type = OrderedListType::UpperRoman;
			++count;
		}
	} else {
		return OrderedListType::None;
	}

	++pos; // move to chNext
	if (count == 2) {
		while (true) {
			chNext = static_cast<uint8_t>(styler[++pos]);
			const bool handled = (type == OrderedListType::Decimal) ? IsADigit(chNext)
				: ((type == OrderedListType::LowerRoman) ? IsLowerRoman(chNext) : IsUpperRoman(chNext));
			if (handled) {
				++count;
				if (count == 10) {
					return OrderedListType::None;
				}
			} else {
				break;
			}
		}
	}

	if ((chNext == '.' && current != '(') || chNext == ')') {
		const uint8_t after = styler[++pos];
		if (IsASpace(after)) {
			if (type == OrderedListType::UpperAlpha && chNext == '.' && count == 1) {
				// single uppercase letter ended with period requires at least two spaces.
				// see https://pandoc.org/MANUAL.html#fn1
				if (!(IsSpaceOrTab(after) && IsASpace(styler[pos + 1]))) {
					return OrderedListType::None;
				}
			}
			if (current == '(') {
				type = static_cast<OrderedListType>(static_cast<int>(type) | (static_cast<int>(OrderedListType::Parenthesis)));
			}
			return type;
		}
	}
	return OrderedListType::None;
}

bool MarkdownLexer::CheckDefinitionList(Sci_PositionU startPos, uint32_t lineState) const noexcept {
	if ((lineState & (LineStateBlockMask ^ LineStateListItemFirstLine)) != 0) {
		return false; // no term on previous line
	}

	Sci_Line termLine = sc.currentLine;
	Sci_PositionU endPos = sc.lineStartNext;
	if (startPos < endPos) {
		if (termLine == 0) {
			return false;
		}
		--termLine;
	} else {
		endPos = sc.styler.LineStart(termLine + 2);
	}
	if (LexGetNextChar(sc.styler, startPos + 2, endPos) == '\0') {
		return false; // empty definition after ':' + space
	}
	if (lineState & LineStateEmptyLine) {
		return true; // teat as multiple definitions
	}

	const int style = GetBlockStyle(sc.styler, termLine, lineState);
	return style == SCE_MARKDOWN_DEFINITION_LIST || !IsBlockStyle(style);
}

// 4.4 Indented code blocks
uint32_t MarkdownLexer::HighlightIndentedText(uint32_t lineState, int indentCount) {
	// prefer indented list item
	if (indentCount == 4 && indentParent != 0) {
		if (sc.ch == '+' || sc.ch == '-' || sc.ch == '*') {
			if (IsASpace(sc.chNext)) {
				sc.SetState(SCE_MARKDOWN_BULLET_LIST);
				return lineState;
			}
		} else if (sc.ch == ':' || sc.ch == '~') {
			if (markdown == Markdown::Pandoc && IsSpaceOrTab(sc.chNext) && CheckDefinitionList(sc.currentPos, lineState)) {
				sc.SetState(SCE_MARKDOWN_DEFINITION_LIST);
				return lineState;
			}
		} else {
			const OrderedListType listType = CheckOrderedList(sc.styler, sc.currentPos, sc.ch, sc.chNext);
			if (IsStandardList(listType, markdown)) {
				sc.SetState(SCE_MARKDOWN_ORDERED_LIST);
				return lineState;
			}
		}
	}

	// check paragraph continuation text
	if (lineState & LineStateListItemFirstLine) {
		return lineState;
	}
	if (sc.currentLine != 0 && ((lineState & LineStateBlockMask) == 0 || sc.ch == '<')) {
		const int style = GetBlockStyle(sc.styler, sc.currentLine - 1, lineState);
		// inner html block
		if (sc.ch == '<' && (IsHtmlBlockStyle(style) || style == SCE_H_TAG) && HandleHtmlTag(HtmlTagType::Both)) {
			return lineState;
		}
		if ((lineState & LineStateBlockMask) == 0 && !IsBlockStyle(style)) {
			return lineState;
		}
	}

	// indented code block
	sc.SetState(SCE_MARKDOWN_INDENTED_BLOCK);
	// for single line indented code block
	if (IsIndentedBlockEnd()) {
		lineState |= LineStateBlockEndLine;
	}
	return lineState;
}

bool MarkdownLexer::IsIndentedBlockEnd() const noexcept {
	// only check indentCount on next two lines to keep trailing blank lines in current block
	bool next = false;
	int indentCount = 0;
	Sci_PositionU pos = sc.lineStartNext;
	const Sci_PositionU endPos = sc.styler.Length();
	while (true) {
		const uint8_t ch = sc.styler[pos++];
		if (ch == ' ') {
			++indentCount;
		} else if (ch == '\t') {
			indentCount = GetTabIndentCount(indentCount);
		} else if (IsEOLChar(ch) || pos >= endPos) {
			if (!next && pos < endPos) {
				next = true;
				indentCount = 0;
				pos = sc.styler.LineStart(sc.currentLine + 2);
				continue;
			}
			return false;
		} else {
			break;
		}
	}
	return indentCount < indentParent + 4;
}

// 5 Container blocks
int MarkdownLexer::GetListChildIndentCount(int indentCurrent) const noexcept {
	Sci_PositionU pos = sc.currentPos;
	indentCurrent += static_cast<int>(pos - sc.styler.GetStartSegment());
	if (sc.ch == '\t') {
		indentCurrent += 4;
	} else if (sc.ch == ' ') {
		int count = 1;
		while (count < 4) {
			const uint8_t ch = sc.styler[++pos];
			if (ch != ' ') {
				break;
			}
			++count;
		}
		indentCurrent += count;
	}
	return indentCurrent;
}

int MarkdownLexer::UpdateParentIndentCount(int indentCurrent) noexcept {
	int indentCount = indentCurrent;
	Sci_Line line = sc.currentLine;
	while (line != 0) {
		--line;
		const uint32_t lineState = sc.styler.GetLineState(line);
		const auto tag = static_cast<HtmlTagState>(lineState & LineStateHtmlTagMask);
		if (tag != HtmlTagState::None) {
			continue;
		}
		if (lineState & LineStateEmptyLine) {
			if (indentCount >= 0 && indentCount < MinContainerIndentChild) {
				break;
			}
		} else {
			indentCount = GetIndentCount(lineState);
			if (indentCurrent < 0) {
				indentCurrent = indentCount;
			} else if (lineState & LineStateListItemFirstLine) {
				if (indentCount < indentCurrent) {
					indentParent = GetIndentChild(lineState);
					return indentCurrent;
				}
			}
			if (indentCount < MinContainerIndentChild) {
				if (lineState & LineStateBlockMask) {
					break;
				}
				const int style = GetBlockStyle(sc.styler, line, lineState);
				if (IsBlockStyle(style)) {
					break;
				}
			}
		}
	}

	indentParent = 0;
	return indentCurrent;
}

int MarkdownLexer::GetCurrentDelimiterRun(DelimiterRun &delimiterRun, bool ignoreCurrent) const noexcept {
	int chPrev = sc.chPrev;
	int delimiter = sc.ch;
	Sci_PositionU pos = sc.currentPos;
	if (ignoreCurrent) {
		chPrev = delimiter;
		delimiter = sc.chNext;
		pos += sc.width;
	}

	// unlike official Lexilla, for performance reason our StyleContext
	// for UTF-8 encoding is byte oriented instead of character oriented.
	if ((chPrev & 0x80) != 0 && sc.styler.Encoding() == EncodingType::unicode) {
		// get full character before delimiter
		const Sci_Position position = sc.styler.GetRelativePosition(pos, -1);
		chPrev = sc.styler.GetCharacterAt(position);
	}

	const Sci_PositionU startPos = pos;
	int chNext = GetCharAfterDelimiter(sc.styler, pos, delimiter);
	if (chNext & 0x80) {
		chNext = sc.styler.GetCharacterAt(pos);
	}

	// not same as Unicode whitespace or punctuation character defined in "2.1 Characters and lines",
	// which can be implemented for UTF-8 encoding with CategoriseCharacter() from CharacterCategory.h
	delimiterRun.ccPrev = (chPrev == '_') ? CharacterClass::punctuation : sc.styler.GetCharacterClass(chPrev);
	delimiterRun.ccNext = (chNext == '_') ? CharacterClass::punctuation : sc.styler.GetCharacterClass(chNext);
	// returns length of the delimiter run
	return static_cast<int>(pos - startPos);
}

constexpr bool IsEmphasisDelimiter(int ch) noexcept {
	return ch == '*' || ch == '_' || ch == '~';
}

constexpr uint8_t GetEmphasisDelimiter(int state) noexcept {
	if constexpr (SCE_MARKDOWN_EM_ASTERISK & 1) {
		return (state == SCE_MARKDOWN_STRIKEOUT) ? '~' : ((state & 1) ? '*' : '_');
	} else {
		return (state == SCE_MARKDOWN_STRIKEOUT) ? '~' : ((state & 1) ? '_' : '*');
	}
}

SeekStatus MarkdownLexer::HighlightEmphasis(uint32_t lineState) {
	HighlightResult result = HighlightResult::None;
	const int current = sc.state;
	const int delimiter = GetEmphasisDelimiter(current);
	if (sc.ch == delimiter && (current != SCE_MARKDOWN_STRIKEOUT || sc.chNext == '~')) {
		DelimiterRun delimiterRun;
		const int length = GetCurrentDelimiterRun(delimiterRun);

		const bool closed = delimiterRun.CanClose(delimiter);
		result = closed ? HighlightResult::Finish : HighlightResult::Continue;
		if (current != SCE_MARKDOWN_STRIKEOUT) {
			// TODO: fix longest match failure for `***strong** in emph* t`
			if (length == 1 && current >= SCE_MARKDOWN_STRONG_ASTERISK) {
				// inner emphasis with `*`
				result = HighlightResult::None;
			} else if (!closed || (current < SCE_MARKDOWN_STRONG_ASTERISK && length == 2)) {
				if (delimiterRun.CanOpen(delimiter)) {
					// inner strong emphasis with `**`
					result = HighlightResult::None;
				}
			}
		}
	} else if (sc.atLineEnd && IsMultilineEnd(lineState)) {
		result = HighlightResult::Invalid;
	} else if (bracketCount != 0) {
		// [] inside link text must be balanced regardless of emphasis
		if (sc.ch == '[') {
			++bracketCount;
			return SeekStatus::None;
		}
		if (sc.ch == ']') {
			if (bracketCount > 1) {
				--bracketCount;
				return SeekStatus::None;
			}
			result = HighlightResult::Invalid;
		}
	}

	if (result != HighlightResult::None) {
		const int outer = TakeOuterStyle();
		const Sci_PositionU startPos = TakeOuterStart();
		if (result == HighlightResult::Finish) {
			if (current >= SCE_MARKDOWN_STRONG_ASTERISK) {
				sc.Forward();
			}
			sc.ForwardSetState(outer);
			return SeekStatus::Continue;
		}

		sc.ChangeState(outer);
		// no rewind inside link text to avoid extra stack for bracketCount.
		if (bracketCount == 0) {
			const bool multiline = sc.BackTo(startPos);
			sc.Forward();
			if ((current == SCE_MARKDOWN_STRIKEOUT)
				|| (result == HighlightResult::Continue && current >= SCE_MARKDOWN_STRONG_ASTERISK)) {
				sc.Forward();
			}
			return multiline ? SeekStatus::Multiline : SeekStatus::Continue;
		}
		return SeekStatus::Continue;
	}

	HighlightInlineText();
	return SeekStatus::None;
}

// 4.7 Link reference definitions
inline bool IsLinkReferenceDefinition(LexAccessor &styler, Sci_Line line, Sci_PositionU startPos) noexcept {
	Sci_PositionU pos = styler.LineStart(line);
	const uint8_t ch = GetCharAfterSpace(styler, pos, 3);
	return ch == '[' && pos == startPos;
}

SeekStatus MarkdownLexer::HighlightLinkText(uint32_t lineState) {
	if (sc.ch == ']') {
		sc.Forward();
		Sci_PositionU startPos = 0;
		--bracketCount;
		if (bracketCount == 0) {
			startPos = TakeOuterStart();
			sc.SetState(TakeOuterStyle());
		}
		if (sc.ch == '(' || (sc.ch == ':' && bracketCount == 0 && nestedState.empty() && sc.state == SCE_MARKDOWN_DEFAULT
			&& IsLinkReferenceDefinition(sc.styler, sc.currentLine, startPos))) {
			startPos = sc.currentPos;
			int chNext = sc.GetLineNextChar(true);
			if (chNext == '\0' && bracketCount == 0 && !OnHeaderLine()) {
				// link destination on next line
				startPos = sc.lineStartNext;
				const Sci_PositionU endPos = sc.styler.LineStart(sc.currentLine + 2);
				chNext = LexGetNextChar(sc.styler, startPos, endPos);
			}
			if (chNext != '\0') {
				SaveOuterStyle(sc.state);
				SaveOuterStart(sc.currentPos);
				tagState = (startPos == sc.currentPos) ? HtmlTagState::None : HtmlTagState::Open;
				parenCount = sc.ch == '(';
				const int style = (chNext == '<') ? SCE_MARKDOWN_ANGLE_LINK
					: (parenCount ? SCE_MARKDOWN_PAREN_LINK : SCE_MARKDOWN_PLAIN_LINK);
				sc.SetState(parenCount ? SCE_MARKDOWN_LINK_TEXT : SCE_MARKDOWN_DELIMITER);
				sc.ForwardSetState(style);
				if (tagState == HtmlTagState::None) {
					// skip whitespace between link text and destination
					while (IsSpaceOrTab(sc.ch)) {
						sc.Forward();
					}
					if (sc.ch == '<') {
						sc.Forward();
					}
				} else {
					sc.Advance(startPos - sc.currentPos - 1);
					return SeekStatus::None;
				}
			}
		}
		return SeekStatus::Continue;
	}

	// to estimate the 999 characters limitation on link label,
	// exact character count can be implemented with GetRelativePosition().
	constexpr Sci_PositionU maxLength = 1024;
	const bool invalid = sc.currentPos - backPos.back() >= maxLength;
	if (invalid || sc.atLineEnd) {
		if (!invalid && !IsMultilineEnd(lineState)) {
			return SeekStatus::None;
		}

		bracketCount = 0;
		sc.ChangeState(TakeOuterStyle());
		const bool multiline = sc.BackTo(TakeOuterStart());
		sc.Forward();
		return multiline ? SeekStatus::Multiline : SeekStatus::Continue;
	}
	if (sc.ch == '[' || sc.Match('!', '[')) {
		++bracketCount;
		if (sc.ch == '!') {
			sc.Forward();
		}
	} else {
		HighlightInlineText();
	}
	return SeekStatus::None;
}

// 6.3 Links
constexpr bool IsLinkTitleStyle(int state) noexcept {
	return state == SCE_MARKDOWN_LINK_TITLE_PAREN
		|| state == SCE_MARKDOWN_LINK_TITLE_SQ
		|| state == SCE_MARKDOWN_LINK_TITLE_DQ;
}

SeekStatus MarkdownLexer::HighlightLinkDestination(uint32_t lineState) {
	if (sc.ch == '\\' || ((sc.ch == '&' || sc.ch == ':') && IsLinkTitleStyle(sc.state))) {
		// escape sequence, entity, emoji
		HighlightInlineText();
		return SeekStatus::None;
	}

	if (tagState >= HtmlTagState::Open && sc.atLineStart) {
		while (IsSpaceOrTab(sc.ch)) {
			sc.Forward();
		}
		switch (sc.state) {
		case SCE_MARKDOWN_ANGLE_LINK:
			if (tagState == HtmlTagState::Open && sc.ch == '<') {
				sc.Forward();
			}
			break;
		case SCE_MARKDOWN_LINK_TITLE_SQ:
			if (tagState == HtmlTagState::Value && sc.ch == '\'') {
				sc.Forward();
			}
			break;
		case SCE_MARKDOWN_LINK_TITLE_DQ:
			if (tagState == HtmlTagState::Value && sc.ch == '\"') {
				sc.Forward();
			}
			break;
		}
	}

	HighlightResult result = HighlightResult::None;
	switch (sc.state) {
	case SCE_MARKDOWN_ANGLE_LINK:
		if (sc.ch == '>') {
			sc.Forward();
			while (IsSpaceOrTab(sc.ch)) {
				sc.Forward();
			}
			if (parenCount != 0 && sc.ch == ')') {
				--parenCount;
				result = HighlightResult::Finish;
			} else {
				result = (sc.chPrev != '>' || IsEOLChar(sc.ch)) ? HighlightResult::Continue : HighlightResult::Invalid;
			}
		} else if (sc.ch == '<' || IsEOLChar(sc.ch)) {
			result = HighlightResult::Invalid;
		}
		break;

	case SCE_MARKDOWN_PLAIN_LINK:
	case SCE_MARKDOWN_PAREN_LINK:
	case SCE_MARKDOWN_LINK_TITLE_PAREN:
		if (sc.ch == '(') {
			++parenCount;
		} else if (sc.ch == ')') {
			--parenCount;
			if (parenCount == 0 && sc.state != SCE_MARKDOWN_PLAIN_LINK) {
				result = HighlightResult::Finish;
			}
		} else if (sc.state != SCE_MARKDOWN_LINK_TITLE_PAREN && !IsGraphic(sc.ch)) {
			result = HighlightResult::Invalid;
			if (IsASpace(sc.ch)) {
				while (IsSpaceOrTab(sc.ch)) {
					sc.Forward();
				}
				if (sc.ch == ')' && parenCount == 1 && sc.state == SCE_MARKDOWN_PAREN_LINK) {
					--parenCount;
					result = HighlightResult::Finish;
				} else {
					result = HighlightResult::Continue;
				}
			}
		}
		break;

	case SCE_MARKDOWN_LINK_TITLE_SQ:
		if (sc.ch == '\'') {
			result = HighlightResult::Finish;
		}
		break;

	case SCE_MARKDOWN_LINK_TITLE_DQ:
		if (sc.ch == '\"') {
			result = HighlightResult::Finish;
		}
		break;
	}

	switch (result) {
	case HighlightResult::None:
		return SeekStatus::None;

	case HighlightResult::Finish: {
		tagState = HtmlTagState::None;
		parenCount = 0;
		const int current = sc.state;
		const int outer = TakeOuterStyle();
		DropOuterStart();
		if (sc.ch == ')' && current != SCE_MARKDOWN_LINK_TITLE_PAREN) {
			// make brace matching work
			sc.SetState(SCE_MARKDOWN_LINK_TEXT);
		}
		sc.ForwardSetState(outer);
		if (IsLinkTitleStyle(current)) {
			while (IsSpaceOrTab(sc.ch)) {
				sc.Forward();
			}
			if (sc.ch == ')') {
				sc.SetState(SCE_MARKDOWN_LINK_TEXT);
				sc.ForwardSetState(outer);
			}
		}
		return SeekStatus::Continue;
	}

	case HighlightResult::Continue: {
		int marker = sc.ch;
		if (IsEOLChar(marker) && !OnHeaderLine()) {
			// link title on next line
			const Sci_PositionU endPos = sc.styler.LineStart(sc.currentLine + 2);
			marker = LexGetNextChar(sc.styler, sc.lineStartNext, endPos);
		}
		if (marker == '\'' || marker == '\"' || marker == '(') {
			const int style = (marker == '(') ? SCE_MARKDOWN_LINK_TITLE_PAREN
				: ((marker == '\'') ? SCE_MARKDOWN_LINK_TITLE_SQ : SCE_MARKDOWN_LINK_TITLE_DQ);
			sc.SetState(style);
			parenCount = sc.ch == '(';
			if (marker != sc.ch) {
				tagState = HtmlTagState::Value;
			} else {
				sc.Forward();
			}
			return SeekStatus::Continue;
		}
		if (IsEOLChar(sc.ch)) {
			if (marker == '\0' || IsMultilineEnd(lineState)) {
				// no link title
				tagState = HtmlTagState::None;
				sc.SetState(TakeOuterStyle());
				DropOuterStart();
				return SeekStatus::Continue;
			}
		}
	}
		[[fallthrough]];
	default: {
		// invalid link destination or title
		tagState = HtmlTagState::None;
		parenCount = 0;
		sc.ChangeState(TakeOuterStyle());
		const bool multiline = sc.BackTo(TakeOuterStart());
		sc.Forward();
		return multiline ? SeekStatus::Multiline : SeekStatus::Continue;
	}
	}
}

// 6.9 Autolinks (extension)
bool MarkdownLexer::DetectAutoLink() {
	int offset = 0;
	AutoLink result = AutoLink::None;
	Sci_PositionU pos = sc.currentPos;
	switch (sc.ch) {
	case 'w':
		if (sc.chNext == 'w' && !IsIdentifierChar(sc.chPrev)
			&& sc.styler[pos + 2] == 'w'
			&& sc.styler[pos + 3] == '.'
			&& IsDomainNameChar(sc.styler[pos + 4])) {
			offset = 3;
			result = AutoLink::Domain;
		}
		break;

	case ':':
		if (sc.chNext == '/' && pos >= 2 && IsSchemeNameChar(sc.chPrev) && sc.styler[pos + 2] == '/'
			&& !IsInvalidUrlChar(sc.styler[pos + 3])) {
			// backtrack to find scheme name before `://`, this is more efficient than forward check every word
			constexpr int kMinSchemeNameLength = 2;
			constexpr int kMaxSchemeNameLength = 32;
			const Sci_PositionU startPos = sc.styler.GetStartSegment();
			const Sci_PositionU endPos = pos;
			pos -= 2;
			uint8_t ch;
			while (true) {
				ch = sc.styler.SafeGetCharAt(pos);
				if (pos == startPos || pos + kMaxSchemeNameLength < endPos || !IsSchemeNameChar(ch)) {
					break;
				}
				--pos;
			}
			if (sc.styler.IsLeadByte(ch)) {
				++pos;
			}

			uint8_t chPrev = '\0';
			while (pos < endPos && !IsAlpha(ch)) {
				chPrev = ch;
				ch = sc.styler[++pos];
			}

			offset = static_cast<int>(endPos - pos);
			if (offset >= kMinSchemeNameLength && !IsIdentifierChar(chPrev)) {
				offset += 2;
				result = AutoLink::Path;
				// go back to scheme start position and change style from here
				sc.currentPos = pos;
			}
		}
		break;
	}

	if (result != AutoLink::None) {
		periodCount = 0;
		autoLink = result;
		SaveOuterStyle(sc.state);
		sc.SetState((sc.state == SCE_H_COMMENT)? STYLE_COMMENT_LINK : STYLE_LINK);
		sc.Advance(offset);
		return true;
	}
	return false;
}

bool MarkdownLexer::HighlightAutoLink() {
	switch (autoLink) {
	case AutoLink::Angle:
		// absolute URI or email
		if (sc.ch == ':' || sc.ch == '@') {
			++periodCount;
		}
		if (IsInvalidUrlChar(sc.ch)) {
			const int count = periodCount;
			periodCount = 0;
			autoLink = AutoLink::None;
			const int outer = TakeOuterStyle();
			if (sc.ch == '>' && count != 0) {
				sc.ForwardSetState(outer);
			} else {
				sc.ChangeState(outer);
				sc.Rewind();
				sc.Forward();
			}
			return true;
		}
		break;

	case AutoLink::Domain:
		if (sc.ch == '.' && IsDomainNameChar(sc.chNext)) {
			++periodCount;
			sc.Forward();
		} else if (!IsDomainNameChar(sc.ch)) {
			const bool invalid = periodCount == 0;
			periodCount = 0;
			if (!invalid && ((sc.ch == ':' && IsADigit(sc.chNext))
				|| ((sc.ch == '/' || sc.ch == '?' || sc.ch == '#') && !IsInvalidUrlChar(sc.chNext)))) {
				autoLink = AutoLink::Path;
			}
			if (autoLink != AutoLink::Path) {
				autoLink = AutoLink::None;
				const int outer = TakeOuterStyle();
				if (invalid) {
					sc.ChangeState(outer);
					sc.Rewind();
					sc.Forward();
				} else {
					if (sc.ch == '/') {
						sc.Forward();
					}
					sc.SetState(outer);
				}
				return true;
			}
		}
		break;

	default: {
		if (sc.ch == '(') {
			++periodCount;
		} else if (sc.ch == ')') {
			--periodCount;
		}
		bool invalid = (sc.chNext == ')' && periodCount == 0) || IsInvalidUrlChar(sc.chNext);
		const bool punctuation = IsPunctuation(sc.ch);
		if (punctuation && !invalid) {
			const int outer = nestedState.back();
			switch (outer) {
			default:
				if (outer == SCE_MARKDOWN_DEFAULT || (outer >= SCE_MARKDOWN_HEADER1 && !IsRawTextStyle(outer))) {
					const bool current = IsEmphasisDelimiter(sc.ch);
					if (current || IsEmphasisDelimiter(sc.chNext)) {
						// similar to HighlightEmphasis()
						DelimiterRun delimiterRun;
						const int length = GetCurrentDelimiterRun(delimiterRun, !current);
						const int delimiter = current ? sc.ch : sc.chNext;
						if ((delimiter != '~' || length >= 2)
							&& (delimiterRun.CanOpen(delimiter) || delimiterRun.CanClose(delimiter))) {
							invalid = true;
						} else {
							sc.Advance(length - current);
						}
					}
				}
				break;

			case SCE_H_SINGLESTRING:
			case SCE_H_SGML_SIMPLESTRING:
				invalid = sc.ch == '\'';
				break;

			case SCE_H_COMMENT:
				if (sc.Match('-', '-')) {
					const char chNext = sc.styler[sc.currentPos + 2];
					if (chNext == '>' || chNext == '?') {
						invalid = true;
					}
				}
				break;

			case SCE_H_CDATA:
				if (sc.Match(']', ']', '>')) {
					invalid = true;
				}
				break;
			}
		}
		if (invalid) {
			if (!punctuation || sc.ch == '/' || (sc.ch == ')' && periodCount == 0)) {
				sc.Forward();
			}
			periodCount = 0;
			autoLink = AutoLink::None;
			sc.SetState(TakeOuterStyle());
			return true;
		}
	} break;
	}

	return false;
}

// 4.6 HTML blocks
HtmlTagType MarkdownLexer::CheckHtmlBlockTag(Sci_PositionU pos, Sci_PositionU endPos, int chNext, bool paragraph) const noexcept {
	char tag[MaxKeywordSize]{};
	pos += 1 + (chNext == '/');
	sc.styler.GetRangeLowered(pos, endPos, tag, sizeof(tag) - 1);
	char *ptr = tag;
	while (IsLowerCase(*ptr)) {
		++ptr;
	}
	if (IsADigit(*ptr)) {
		++ptr;
	}

	uint8_t ch = *ptr;
	bool complete = (ch == '>') || (chNext != '/' && (IsASpace(ch) || (ch == '/' && ptr[1] == '>')));
	if (complete) {
		// check type 1 and type 6 tag
		*ptr = '\0';
		if (blockTagList.InList(tag)) {
			return HtmlTagType::Block;
		}
	}
	if (!paragraph) {
		// type 7 may not interrupt a paragraph
		pos += ptr - tag;
		if (!complete) {
			while (true) {
				ch = sc.styler.SafeGetCharAt(pos++);
				if (!IsHtmlTagChar(ch)) {
					complete = (ch == '>') || (chNext != '/' && (IsASpace(ch) || (ch == '/' && sc.styler[pos] == '>')));
					break;
				}
			}
		}
		if (complete) {
			complete = chNext == '/';
			pos += ch == '/';
			uint8_t quote = '\0'; // ignore character inside attribute string
			while (pos < endPos) {
				ch = sc.styler.SafeGetCharAt(pos++);
				if (!IsASpace(ch)) {
					if (complete) {
						return HtmlTagType::Both;
					}
					if (ch == '\'' || ch == '\"') {
						if (quote == '\0') {
							quote = ch;
						} else if (quote == ch) {
							quote = '\0';
						}
					} else if (ch == '>' && quote == '\0') {
						complete = true;
					}
				}
			}
			return complete ? HtmlTagType::Block : HtmlTagType::Both;
		}
	}
	return HtmlTagType::Inline;
}

bool MarkdownLexer::HandleHtmlTag(HtmlTagType tagType) {
	const int current = sc.state;
	if (sc.chNext == '!') {
		const int chNext = sc.GetRelative(2);
		if (chNext == '-' && sc.GetRelative(3) == '-') {
			sc.SetState(SCE_H_COMMENT);
			sc.Advance(3);
			// handle empty comment: <!-->, <!--->
			// https://html.spec.whatwg.org/multipage/parsing.html#parse-error-abrupt-closing-of-empty-comment
			if (sc.chNext == '>' || sc.MatchNext('-', '>')) {
				sc.Forward((sc.chNext == '>') ? 2 : 3);
				sc.SetState(current);
				return true;
			}
		} else if (chNext == '[' && sc.styler.Match(sc.currentPos + 3, "CDATA[")) {
			// <![CDATA[ ]]>
			sc.SetState(SCE_H_CDATA);
			sc.Advance(8);
		} else if (IsAlpha(chNext)) {
			// <!DOCTYPE html>
			sc.SetState(SCE_H_SGML_COMMAND);
		} else if (tagType == HtmlTagType::Inline) {
			sc.SetState(STYLE_LINK);
		}
	} else if (sc.chNext == '?') {
		// <?php ?>
		sc.SetState(SCE_H_QUESTION);
	} else if (IsHtmlTagStart(sc.chNext) || (sc.chNext == '/' && IsHtmlTagStart(sc.GetRelative(2)))) {
		if (tagType != HtmlTagType::Inline) {
			const HtmlTagType result = CheckHtmlBlockTag(sc.currentPos, sc.lineStartNext, sc.chNext, false);
			switch (result) {
			case HtmlTagType::Inline:
				return false;
			case HtmlTagType::Block:
				tagType = HtmlTagType::Block;
				break;
			case HtmlTagType::Both:
				if (tagType == HtmlTagType::Block) {
					return false;
				}
				tagType = HtmlTagType::Inline;
				break;
			}
		}
		sc.SetState(SCE_H_TAG);
		if (sc.chNext == '/') {
			sc.Forward();
		} else {
			tagState = HtmlTagState::Open;
		}
		if (tagType == HtmlTagType::Block) {
			sc.ForwardSetState(SCE_H_BLOCK_TAG);
		}
	} else if (tagType == HtmlTagType::Inline && !IsInvalidUrlChar(sc.chNext)) {
		sc.SetState(STYLE_LINK);
	}
	if (current != sc.state) {
		if (tagType == HtmlTagType::Inline) {
			if (sc.state == STYLE_LINK) {
				autoLink = AutoLink::Angle;
				periodCount = 0;
			}
			SaveOuterStyle(current);
		}
		return true;
	}
	return false;
}

// https://pandoc.org/MANUAL.html#math
constexpr bool IsMathOpenDollar(int chNext) noexcept {
	return !IsASpace(chNext);
}

constexpr bool IsMathCloseDollar(int chPrev, int chNext) noexcept {
	return !IsASpace(chPrev) && !IsADigit(chNext);
}

bool MarkdownLexer::IsParagraphEnd(Sci_PositionU pos, uint32_t lineState) const noexcept {
	int indentCount = indentParent;
	if (lineState & LineStateListItemFirstLine) {
		indentCount = GetIndentChild(lineState);
	}
	indentCount += 4;
	uint8_t ch = sc.styler.SafeGetCharAt(pos);
	while (IsSpaceOrTab(ch) && indentCount > 0) {
		if (ch == ' ') {
			--indentCount;
		} else {
			indentCount -= 4;
		}
		ch = sc.styler[++pos];
	}

	const uint8_t chNext = sc.styler.SafeGetCharAt(pos + 1);
	if (indentCount <= 0) {
		if (indentCount == 0 && ch > ' ' && (indentParent != 0 || (lineState & LineStateListItemFirstLine) != 0)) {
			// check indented list item
			if (ch == '+' || ch == '-' || ch == '*') {
				return IsASpace(chNext);
			}
			if (ch == ':' || ch == '~') { // definition list
				return markdown == Markdown::Pandoc && IsSpaceOrTab(chNext) && CheckDefinitionList(pos, lineState);
			}
			const OrderedListType listType = CheckOrderedList(sc.styler, pos, ch, chNext);
			return IsStandardList(listType, markdown);
		}
		// TODO: check indented code block
		while (IsSpaceOrTab(ch)) {
			ch = sc.styler[++pos];
		}
		return IsEOLChar(ch); // empty line
	}

	switch (ch) {
	case '\r':	// empty line
	case '\n':	// empty line
	case '>':	// block quote
		return true;

	case '#':
		if (IsASpace(chNext)) {
			return true;
		}
		if (chNext == '#') {
			return CheckATXHeading(sc.styler, pos);
		}
		break;

	case '<':
		if (chNext == '?') {
			return true;
		}
		if (chNext == '!') {
			ch = sc.styler[pos + 2];
			switch (ch) {
			case '-':
				return sc.styler[pos + 3] == '-';
			case '[':
				return sc.styler.Match(pos + 3, "CDATA[");
			default:
				return IsAlpha(ch);
			}
		}
		return (IsHtmlTagStart(chNext) || chNext == '/') && CheckHtmlBlockTag(pos, sc.styler.Length(), chNext, true) == HtmlTagType::Block;

	case '+':
	case '-':
	case '*':
	case '_':
		if (ch != '+' && CheckThematicBreak(sc.styler, pos, ch)) {
			return true;
		}
		if (ch != '_') { // bullet list
			return IsASpace(chNext);
		}
		[[fallthrough]];
	case '=':
		return CheckSetextHeading(sc.styler, pos);

	case '`':
	case '~': // definition list
	case ':': // definition list
		if (ch != ':' && ch == chNext) { // fenced code block
			return static_cast<uint8_t>(sc.styler[pos + 2]) == chNext;
		}
		return markdown == Markdown::Pandoc && ch != '`' && IsSpaceOrTab(chNext) && CheckDefinitionList(pos, lineState);

	case '[': // link reference definition
		return indentParent == 0 && !IsEOLChar(chNext);

	case '$':
		return chNext == '$' && markdown != Markdown::GitLab;
	}

	const OrderedListType listType = CheckOrderedList(sc.styler, pos, ch, chNext);
	return IsStandardList(listType, markdown);
}

int MarkdownLexer::HighlightBlockText(uint32_t lineState) {
	switch (sc.ch) {
	case '>':
		if (markdown == Markdown::GitLab && sc.atLineStart && sc.MatchNext('>', '>')) {
			sc.SetState(SCE_MARKDOWN_MULTI_BLOCKQUOTE);
		} else {
			sc.SetState(SCE_MARKDOWN_BLOCKQUOTE);
		}
		return 0;

	case '+':
	case '-':
	case '*':
	case '_':
		if (sc.ch != '+' && CheckThematicBreak(sc.styler, sc.currentPos, sc.ch)) {
			sc.SetState(SCE_MARKDOWN_HRULE);
			return 0;
		}
		if (sc.ch != '_' && IsASpace(sc.chNext)) {
			sc.SetState(SCE_MARKDOWN_BULLET_LIST);
			return 0;
		}
		break;

	case '#':
		if (IsASpace(sc.chNext)) {
			sc.SetState(SCE_MARKDOWN_HEADER1);
			return SCE_MARKDOWN_HEADER1;
		}
		if (sc.chNext == '#') {
			int headerLevel = CheckATXHeading(sc.styler, sc.currentPos);
			if (headerLevel != 0) {
				headerLevel += SCE_MARKDOWN_HEADER1 - 1;
				sc.SetState(headerLevel);
				return headerLevel;
			}
		}
		break;

	case '`':
	case '~':
		if (sc.ch == sc.chNext) {
			Sci_PositionU pos = sc.currentPos;
			const int chNext = GetCharAfterDelimiter(sc.styler, pos, sc.ch);
			const int count = static_cast<int>(pos - sc.currentPos);
			if (count >= 3) {
				delimiterCount = count;
				int style = (sc.ch == '`') ? SCE_MARKDOWN_BACKTICK_BLOCK : SCE_MARKDOWN_TILDE_BLOCK;
				// check info string
				if (AnyOf<'L', 'l', 'M', 'm'>(chNext)) {
					char info[8]{};
					sc.styler.GetRangeLowered(pos, sc.lineStartNext, info, sizeof(info));
					if (StrStartsWith(info, "math") || StrStartsWith(info, "latex")) {
						style += SCE_MARKDOWN_BACKTICK_MATH - SCE_MARKDOWN_BACKTICK_BLOCK;
					}
				}
				sc.SetState(style);
				return 0;
			}
		}
		[[fallthrough]];
	case ':':
		if (markdown == Markdown::Pandoc && sc.ch != '`' && IsSpaceOrTab(sc.chNext) && CheckDefinitionList(sc.currentPos, lineState)) {
			sc.SetState(SCE_MARKDOWN_DEFINITION_LIST);
			return 0;
		}
		break;

	case '<':
		if (HandleHtmlTag(HtmlTagType::Block)) {
			return 0;
		}
		break;

	case '$':
		if (sc.chNext == '$' && markdown != Markdown::GitLab) {
			sc.SetState(SCE_MARKDOWN_DISPLAY_MATH);
			return 0;
		}
		break;
	}

	const OrderedListType listType = CheckOrderedList(sc.styler, sc.currentPos, sc.ch, sc.chNext);
	if (IsStandardList(listType, markdown)) {
		sc.SetState(SCE_MARKDOWN_ORDERED_LIST);
		return 0;
	}
	// setext heading has lower precedence than other blocks
	int headerLevel = CheckSetextHeading(sc.styler, sc.lineStartNext);
	if (headerLevel != 0) {
		headerLevel += SCE_MARKDOWN_SETEXT_H1 - 1;
		sc.SetState(headerLevel);
		HighlightInlineText();
		return headerLevel;
	}
	if (listType != OrderedListType::None) {
		// nonstandard ordered list
		sc.SetState(SCE_MARKDOWN_EXT_ORDERED_LIST);
		return 0;
	}

	switch (sc.ch) {
	case '~':
		if (IsSpaceOrTab(sc.chNext)) {
			// Pandoc definition list
			SaveOuterStyle(SCE_MARKDOWN_DEFAULT);
			sc.SetState(SCE_MARKDOWN_DELIMITER);
		}
		break;

	case '+':
	case '=':
		if (markdown == Markdown::Pandoc) {
			HighlightDelimiterRow();
		}
		break;

	case '|':
	case ':':
	case '-':
		HighlightDelimiterRow();
		break;
	}
	return 0;
}

void MarkdownLexer::HighlightInlineText() {
	bool handled = false;
	const int current = sc.state;
	switch (sc.ch) {
	case '\\':
		if (IsPunctuation(sc.chNext)) {
			sc.SetState(SCE_MARKDOWN_ESCAPECHAR);
			sc.Forward();
		}
		break;

	case '&':
		if (IsAlpha(sc.chNext) || sc.chNext == '#') {
			sc.SetState(SCE_H_ENTITY);
			if (sc.chNext == '#') {
				sc.Forward();
			}
		}
		break;

	case '<':
		if (HandleHtmlTag(HtmlTagType::Inline)) {
			return;
		}
		break;

	case '`':
		delimiterCount = GetMatchedDelimiterCount(sc.styler, sc.currentPos, '`');
		sc.SetState(SCE_MARKDOWN_CODE_SPAN);
		sc.Advance(delimiterCount - 1);
		break;

	case '*':
	case '_':
	case '~':
		if (sc.ch != '~' || sc.chNext == '~') {
			DelimiterRun delimiterRun;
			GetCurrentDelimiterRun(delimiterRun);
			const int delimiter = sc.ch;
			if (delimiterRun.CanOpen(delimiter)) {
				handled = true;
				int style = SCE_MARKDOWN_STRIKEOUT;
				if (delimiter != '~') {
					style = (delimiter == '*') ? SCE_MARKDOWN_EM_ASTERISK : SCE_MARKDOWN_EM_UNDERSCORE;
					if (delimiter == sc.chNext) {
						style += SCE_MARKDOWN_STRONG_ASTERISK - SCE_MARKDOWN_EM_ASTERISK;
					}
				}
				SaveOuterStart(sc.currentPos);
				sc.SetState(style);
			}
			if (delimiter == sc.chNext) {
				sc.Forward(); // longest match
			}
		} else if (markdown == Markdown::Pandoc && IsGraphic(sc.chNext)) {
			sc.SetState(SCE_MARKDOWN_SUBSCRIPT);
		}
		break;

	case '{':
	case '[':
		if (markdown == Markdown::GitLab && current == SCE_MARKDOWN_DEFAULT && (sc.chNext == '+' || sc.chNext == '-')) {
			// https://docs.gitlab.com/ee/user/markdown.html#inline-diff
			int style = (sc.chNext == '+') ? SCE_MARKDOWN_DIFF_ADD_CURLY : SCE_MARKDOWN_DIFF_DEL_CURLY;
			if (sc.ch == '[') {
				style += SCE_MARKDOWN_DIFF_ADD_SQUARE - SCE_MARKDOWN_DIFF_ADD_CURLY;
			}
			SaveOuterStart(sc.currentPos);
			sc.SetState(style);
			sc.Forward();
		} else if (sc.ch == '[') {
			if (current == SCE_MARKDOWN_DEFAULT && IsSpaceOrTab(sc.chPrev)
				&& (sc.chNext == ' ' || sc.chNext == 'x' || sc.chNext == 'X')
				&& sc.GetRelative(2) == ']' && IsASpace(sc.GetRelative(3))) {
				// task list after list marker
				sc.SetState(SCE_MARKDOWN_TASK_LIST);
				sc.Advance(2);
			} else if (!(sc.chPrev == '!' || sc.GetLineNextChar(true) == '\0')) {
				// chPrev == '!' means parsing image link failed
				bracketCount = 1;
				SaveOuterStart(sc.currentPos);
				sc.SetState(SCE_MARKDOWN_LINK_TEXT);
			}
		}
		break;

	case '!':
		if (sc.chNext == '[') {
			bracketCount = 1;
			SaveOuterStart(sc.currentPos);
			sc.SetState(SCE_MARKDOWN_LINK_TEXT);
			sc.Forward();
		}
		break;

	case ':':
		if (IsAlphaNumeric(sc.chNext) || ((sc.chNext == '+' || sc.chNext == '-') && IsADigit(sc.GetRelative(2)))) {
			sc.SetState(SCE_MARKDOWN_EMOJI);
			if (sc.chNext == '+') {
				sc.Forward();
			}
		} else if (sc.chNext == ':' || sc.chPrev == ':') {
			sc.SetState(SCE_MARKDOWN_DELIMITER); // Pandoc fenced divs
		}
		break;

	case '|': // table pipe, Pandoc line block
		sc.SetState(SCE_MARKDOWN_DELIMITER);
		break;

	case '$':
		if (markdown != Markdown::GitLab) {
			if (sc.chNext == '$') {
				sc.SetState(SCE_MARKDOWN_INLINE_DISPLAY_MATH);
			} else if (IsMathOpenDollar(sc.chNext)) {
				sc.SetState(SCE_MARKDOWN_INLINE_MATH);
			}
		} else if (sc.chNext == '`') {
			sc.SetState(SCE_MARKDOWN_MATH_SPAN);
			sc.Forward();
		}
		break;

	case '(':
		if (markdown == Markdown::Pandoc && sc.chNext == '@') {
			sc.SetState(SCE_MARKDOWN_EXAMPLE_LIST);
			sc.Forward();
		}
		break;

	case '^':
		if (markdown == Markdown::Pandoc && IsGraphic(sc.chNext)) {
			sc.SetState(SCE_MARKDOWN_SUPERSCRIPT);
		}
		break;

	case '@':
		if (IsIdentifierChar(sc.chNext) && !IsIdentifierChar(sc.chPrev)) {
			sc.SetState(SCE_MARKDOWN_CITATION_AT);
		}
		break;
	}
	if (handled || current != sc.state) {
		SaveOuterStyle(current);
	} else if (bracketCount == 0) {
		DetectAutoLink();
	}
}

SeekStatus MarkdownLexer::HighlightCodeSpan(uint32_t lineState) {
	HighlightResult result = HighlightResult::None;
	switch (sc.state) {
	case SCE_MARKDOWN_CODE_SPAN:
		if (sc.ch == '`') {
			const int count = GetMatchedDelimiterCount(sc.styler, sc.currentPos, '`');
			sc.Advance(count - 1);
			if (count == delimiterCount) {
				delimiterCount = 0;
				result = HighlightResult::Finish;
			}
		} else if (bracketCount == 0) {
			DetectAutoLink();
		}
		break;

	case SCE_MARKDOWN_MATH_SPAN:
	case SCE_MARKDOWN_INLINE_DISPLAY_MATH:
	case SCE_MARKDOWN_INLINE_MATH:
		if (sc.ch == '\\') {
			sc.Forward();
		} else if (sc.ch == '`') {
			if (sc.state == SCE_MARKDOWN_MATH_SPAN) {
				result = (sc.chNext == '$') ? HighlightResult::Finish : HighlightResult::Invalid;
			}
		} else if (sc.ch == '$') {
			if ((sc.state == SCE_MARKDOWN_INLINE_DISPLAY_MATH && sc.chNext == '$')
				|| (sc.state == SCE_MARKDOWN_INLINE_MATH && IsMathCloseDollar(sc.chPrev, sc.chNext))) {
				result = HighlightResult::Finish;
			}
		}
		break;
	}

	if (result == HighlightResult::None && sc.atLineEnd && IsMultilineEnd(lineState)) {
		result = HighlightResult::Invalid;
	}
	switch (result) {
	case HighlightResult::Finish:
		if (sc.state == SCE_MARKDOWN_MATH_SPAN || sc.state == SCE_MARKDOWN_INLINE_DISPLAY_MATH) {
			sc.Forward();
		}
		sc.ForwardSetState(TakeOuterStyle());
		return SeekStatus::Continue;

	case HighlightResult::Invalid: {
		const Sci_PositionU startPos = sc.styler.GetStartSegment();
		sc.ChangeState(TakeOuterStyle());
		const bool multiline = sc.BackTo(startPos);
		sc.Forward();
		return multiline ? SeekStatus::Multiline : SeekStatus::Continue;
	}

	default:
		return SeekStatus::None;
	}
}

bool MarkdownLexer::HighlightCriticMarkup() {
	if (sc.ch == '\\' || sc.ch == '&' || sc.ch == ':') {
		// escape sequence, entity, emoji
		HighlightInlineText();
	} else if (sc.atLineEnd || sc.ch == '`') {
		sc.ChangeState(nestedState.back());
		(void)sc.BackTo(backPos.back());
		if (sc.ch == '[') {
			bracketCount = 1;
			sc.SetState(SCE_MARKDOWN_LINK_TEXT);
		} else {
			DropOuterStyle();
			DropOuterStart();
			sc.Forward();
			return true;
		}
	} else if (sc.ch == ((sc.state < SCE_MARKDOWN_DIFF_DEL_CURLY) ? '+' : '-')) {
		if (sc.chNext == (((sc.state - SCE_MARKDOWN_DIFF_DEL_CURLY) & 1) ? ']' : '}')) {
			DropOuterStart();
			sc.Forward(2);
			sc.SetState(TakeOuterStyle());
			return true;
		}
	}
	return false;
}

void ColouriseMarkdownDoc(Sci_PositionU startPos, Sci_Position lengthDoc, int initStyle, LexerWordList keywordLists, Accessor &styler) {
	if (startPos != 0) {
		// backtrack to previous line for better coloring on typing.
		const Sci_PositionU endPos = startPos + lengthDoc;
		Sci_Line currentLine = styler.GetLine(startPos) - 1;
		if (currentLine > 0 && (styler.GetLineState(currentLine) & LineStateEmptyLine) != 0) {
			--currentLine; // fix typing after indented code block
		}
		startPos = styler.LineStart(currentLine);
		lengthDoc = endPos - startPos;
		initStyle = 0;
		if (startPos != 0) {
			initStyle = styler.StyleIndexAt(startPos - 1);
			BacktrackToStart(styler, LineStateNestedStateLine, startPos, lengthDoc, initStyle);
		}
	}

	MarkdownLexer lexer(startPos, lengthDoc, initStyle, keywordLists, styler);
	const bool fold = styler.GetPropertyBool("fold");

	StyleContext &sc = lexer.sc;
	uint32_t lineState = 0;
	int visibleChars = 0;
	int indentCurrent = 0;
	int indentPrevious = 0;
	int indentChild = 0;
	int headerLevel = 0;
	int prevLevel = SC_FOLDLEVELBASE;

	if (sc.currentLine > 0) {
		prevLevel = styler.LevelAt(sc.currentLine - 1);
		lineState = styler.GetLineState(sc.currentLine - 1);
		lexer.delimiterCount = (lineState >> 8) & 0xff;
		/*
		2: tagState
		1: empty line
		1: nestedState
		1: list item first line
		1: block end line
		1: setext first line
		1: unused
		8: delimiterCount
		8: indentCurrent
		8: indentChild
		*/
		indentPrevious = lexer.UpdateParentIndentCount(-1);
		indentPrevious = sci::max(indentPrevious, 0);
	}
	if (startPos == 0) {
		switch (sc.ch) {
		case '-': // YAML or others
			if (sc.MatchNext('-', '-')) {
				initStyle = SCE_MARKDOWN_METADATA_YAML;
			}
			break;

		case '+': // TOML
		case ';': // JSON
			if (lexer.markdown == Markdown::GitLab && sc.MatchNext()) {
				initStyle = (sc.ch == '+') ? SCE_MARKDOWN_METADATA_TOML : SCE_MARKDOWN_METADATA_JSON;
			}
			break;

		case '%':// title block
			if (lexer.markdown == Markdown::Pandoc) {
				initStyle = SCE_MARKDOWN_TITLE_BLOCK;
			}
			break;
		}
		if (initStyle != SCE_MARKDOWN_DEFAULT) {
			sc.SetState(initStyle);
			sc.Forward();
		}
	}

	while (sc.More()) {
		if (sc.atLineStart) {
			visibleChars = 0;
			indentCurrent = 0;
			indentChild = 0;
			headerLevel = 0;
			if (lineState & LineStateBlockEndLine) {
				lineState &= ~LineStateBlockEndLine;
				lexer.delimiterCount = 0;
				sc.SetState(SCE_MARKDOWN_DEFAULT);
			}
		}

		const int visibleBefore = visibleChars;
		if (visibleBefore == 0) {
			if (sc.ch == ' ') {
				indentCurrent += 1;
			} else if (sc.ch == '\t') {
				indentCurrent = GetTabIndentCount(indentCurrent);
			} else if (!IsASpace(sc.ch)) {
				visibleChars = 1;
			}
		}

		switch (sc.state) {
		// block
		case SCE_MARKDOWN_HEADER1:
		case SCE_MARKDOWN_HEADER2:
		case SCE_MARKDOWN_HEADER3:
		case SCE_MARKDOWN_HEADER4:
		case SCE_MARKDOWN_HEADER5:
		case SCE_MARKDOWN_HEADER6:
			if (sc.atLineStart) {
				sc.SetState(SCE_MARKDOWN_DEFAULT);
			} else {
				lexer.HighlightInlineText();
			}
			break;

		case SCE_MARKDOWN_SETEXT_H1:
		case SCE_MARKDOWN_SETEXT_H2:
			if (lineState & LineStateSetextFirstLine) {
				lexer.HighlightInlineText();
			} else if (visibleBefore == 0 && sc.ch == ((sc.state == SCE_MARKDOWN_SETEXT_H1) ? '=' : '-')) {
				lineState |= LineStateBlockEndLine;
			}
			break;

		case SCE_MARKDOWN_HRULE:
		case SCE_MARKDOWN_DELIMITER_ROW:
			if (sc.atLineStart) {
				sc.SetState(SCE_MARKDOWN_DEFAULT);
			} else if (sc.ch == '|' || sc.ch == '+') {
				lexer.SaveOuterStyle(SCE_MARKDOWN_DELIMITER_ROW);
				sc.SetState(SCE_MARKDOWN_DELIMITER);
			}
			break;

		case SCE_MARKDOWN_BACKTICK_BLOCK:
		case SCE_MARKDOWN_BACKTICK_MATH:
		case SCE_MARKDOWN_TILDE_BLOCK:
		case SCE_MARKDOWN_TILDE_MATH:
			if (visibleBefore == 0 && indentCurrent < lexer.indentParent + 4
				&& sc.ch == ((sc.state <= SCE_MARKDOWN_BACKTICK_MATH) ? '`' : '~')) {
				const int count = GetMatchedDelimiterCount(styler, sc.currentPos, sc.ch);
				if (count >= lexer.delimiterCount) {
					lineState |= LineStateBlockEndLine;
					break;
				}
			}
			lexer.DetectAutoLink();
			break;

		case SCE_MARKDOWN_INDENTED_BLOCK:
			if (sc.atLineStart) {
				if (lexer.IsIndentedBlockEnd()) {
					lineState |= LineStateBlockEndLine;
				}
			} else {
				lexer.DetectAutoLink();
			}
			break;

		case SCE_MARKDOWN_BLOCKQUOTE:
			if (sc.atLineStart && lexer.IsParagraphEnd(sc.currentPos, lineState)) {
				sc.SetState(SCE_MARKDOWN_DEFAULT);
			} else {
				lexer.HighlightInlineText();
			}
			break;

		case SCE_MARKDOWN_MULTI_BLOCKQUOTE:
			if (sc.atLineStart && sc.Match('>', '>', '>')) {
				lineState |= LineStateBlockEndLine;
			} else {
				lexer.HighlightInlineText();
			}
			break;

		case SCE_MARKDOWN_BULLET_LIST:
		case SCE_MARKDOWN_DEFINITION_LIST:
		case SCE_MARKDOWN_ORDERED_LIST:
		case SCE_MARKDOWN_EXT_ORDERED_LIST:
			if (!IsGraphic(sc.ch)) {
				if (sc.state != SCE_MARKDOWN_EXT_ORDERED_LIST) {
					indentChild = lexer.GetListChildIndentCount(indentCurrent);
					lineState |= LineStateListItemFirstLine | (static_cast<uint32_t>(indentChild) << 24);
				}
				sc.SetState(SCE_MARKDOWN_DEFAULT);
			}
			break;

		case SCE_MARKDOWN_METADATA_YAML:
		case SCE_MARKDOWN_METADATA_TOML:
		case SCE_MARKDOWN_METADATA_JSON:
			if (sc.atLineStart) {
				const int delimiter = static_cast<uint8_t>((sc.state == SCE_MARKDOWN_METADATA_YAML) ? '-'
					: ((sc.state == SCE_MARKDOWN_METADATA_TOML) ? '+' : ';'));
				if ((sc.ch == delimiter || (sc.ch == '.' && sc.state == SCE_MARKDOWN_METADATA_YAML)) && sc.MatchNext()) {
					// `...` YAML document end marker, used by Pandoc
					lineState |= LineStateBlockEndLine;
					break;
				}
			}
			lexer.DetectAutoLink();
			break;

		case SCE_MARKDOWN_TITLE_BLOCK:
			if (sc.atLineStart && !(sc.ch == '%' || (sc.ch == ' ' && sc.GetLineNextChar(true) != '\0'))) {
				sc.SetState(SCE_MARKDOWN_DEFAULT);
			} else {
				lexer.HighlightInlineText();
			}
			break;

		case SCE_MARKDOWN_DISPLAY_MATH:
			if (sc.ch == '\\') {
				sc.Forward();
			} else if (sc.Match('$', '$')) {
				lineState |= LineStateBlockEndLine;
			}
			break;

		// multiline inline
		case SCE_MARKDOWN_EM_ASTERISK:
		case SCE_MARKDOWN_EM_UNDERSCORE:
		case SCE_MARKDOWN_STRONG_ASTERISK:
		case SCE_MARKDOWN_STRONG_UNDERSCORE:
		case SCE_MARKDOWN_STRIKEOUT:

		case SCE_MARKDOWN_LINK_TEXT:
		case SCE_MARKDOWN_ANGLE_LINK:
		case SCE_MARKDOWN_PLAIN_LINK:
		case SCE_MARKDOWN_PAREN_LINK:

		case SCE_MARKDOWN_LINK_TITLE_PAREN:
		case SCE_MARKDOWN_LINK_TITLE_SQ:
		case SCE_MARKDOWN_LINK_TITLE_DQ:

		case SCE_MARKDOWN_CODE_SPAN:
		case SCE_MARKDOWN_MATH_SPAN:
		case SCE_MARKDOWN_INLINE_DISPLAY_MATH:
		case SCE_MARKDOWN_INLINE_MATH: {
			SeekStatus status;
			if (sc.state < SCE_MARKDOWN_LINK_TEXT) {
				status = lexer.HighlightEmphasis(lineState);
			} else if (sc.state == SCE_MARKDOWN_LINK_TEXT) {
				status = lexer.HighlightLinkText(lineState);
			} else if (sc.state < SCE_MARKDOWN_CODE_SPAN) {
				status = lexer.HighlightLinkDestination(lineState);
			} else {
				status = lexer.HighlightCodeSpan(lineState);
			}
			switch (status) {
			case SeekStatus::Continue:
				continue;
			case SeekStatus::Multiline:
				// restore lineState after multiline rewind
				lineState = styler.GetLineState(sc.currentLine);
				indentCurrent = GetIndentCount(lineState);
				indentChild = GetIndentChild(lineState);
				visibleChars = (lineState & LineStateEmptyLine) == 0;
				lexer.tagState = static_cast<HtmlTagState>(lineState & LineStateHtmlTagMask);
				lexer.delimiterCount = (lineState >> 8) & 0xff;
				continue;
			default:
				break;
			}
		} break;

		// single line inline
		case SCE_MARKDOWN_ESCAPECHAR:
		case SCE_MARKDOWN_DELIMITER:
		case SCE_MARKDOWN_TASK_LIST:
			sc.SetState(lexer.TakeOuterStyle());
			continue;

		case STYLE_LINK:
		case STYLE_COMMENT_LINK:
			if (lexer.HighlightAutoLink()) {
				continue;
			}
			break;

		case SCE_MARKDOWN_EMOJI:
		case SCE_MARKDOWN_EXAMPLE_LIST:
		case SCE_MARKDOWN_CITATION_AT:
			if ((sc.ch == ':' && sc.state == SCE_MARKDOWN_EMOJI)
				|| (sc.ch == ')' && sc.state == SCE_MARKDOWN_EXAMPLE_LIST)) {
				sc.ForwardSetState(lexer.TakeOuterStyle());
				continue;
			}
			if (!(IsIdentifierChar(sc.ch) || sc.ch == '-')) {
				const int outer = lexer.TakeOuterStyle();
				if (sc.state == SCE_MARKDOWN_CITATION_AT) {
					sc.SetState(outer);
				} else {
					sc.ChangeState(outer);
					sc.Rewind();
					sc.Forward();
				}
				continue;
			}
			break;

		case SCE_MARKDOWN_SUPERSCRIPT:
		case SCE_MARKDOWN_SUBSCRIPT:
			if (sc.ch == '\\' && IsSpaceOrTab(sc.chNext)) {
				sc.Forward();
			} else if ((sc.ch == '^' && sc.state == SCE_MARKDOWN_SUPERSCRIPT)
				|| (sc.ch == '~' && sc.state == SCE_MARKDOWN_SUBSCRIPT)) {
				sc.ForwardSetState(lexer.TakeOuterStyle());
				continue;
			} else if (!IsGraphic(sc.ch)) {
				sc.Rewind();
				if (IsAlphaNumeric(sc.chNext)) { // MultiMarkdown
					const int style = sc.state + SCE_MARKDOWN_SHORT_SUPERSCRIPT - SCE_MARKDOWN_SUPERSCRIPT;
					sc.ChangeState(style);
				} else {
					sc.ChangeState(lexer.TakeOuterStyle());
					sc.Forward();
					continue;
				}
			}
			break;

		case SCE_MARKDOWN_SHORT_SUPERSCRIPT:
		case SCE_MARKDOWN_SHORT_SUBSCRIPT:
			if (!IsAlphaNumeric(sc.ch)) {
				sc.SetState(lexer.TakeOuterStyle());
				continue;
			}
			break;

		case SCE_MARKDOWN_DIFF_ADD_CURLY:
		case SCE_MARKDOWN_DIFF_ADD_SQUARE:
		case SCE_MARKDOWN_DIFF_DEL_CURLY:
		case SCE_MARKDOWN_DIFF_DEL_SQUARE:
			if (lexer.HighlightCriticMarkup()) {
				continue;
			}
			break;

		// basic html
		case SCE_H_TAG:
		case SCE_H_BLOCK_TAG:
			if (sc.ch == '>' || sc.Match('/', '>')) {
				if (sc.state == SCE_H_BLOCK_TAG) {
					sc.SetState(SCE_H_TAG);
				}
				lexer.tagState = HtmlTagState::None;
				sc.Forward((sc.ch == '/') ? 2 : 1);
				sc.SetState(lexer.TryTakeOuterStyle());
				continue;
			}
			if (!IsHtmlTagChar(sc.ch)) {
				if (IsASpace(sc.ch)) {
					// tag attribute
					sc.SetState(SCE_H_OTHER);
				} else {
					lexer.tagState = HtmlTagState::None;
					lexer.periodCount = 0;
					lexer.autoLink = AutoLink::Angle;
					sc.ChangeState(STYLE_LINK);
					continue;
				}
			}
			break;

		case SCE_H_ENTITY:
			if (!IsAlphaNumeric(sc.ch)) {
				if (sc.ch == ';') {
					sc.Forward();
				} else {
					sc.ChangeState(SCE_H_TAGUNKNOWN);
				}
				sc.SetState(lexer.TakeOuterStyle());
				continue;
			}
			break;

		case SCE_H_ATTRIBUTE:
			if (!IsHtmlAttrChar(sc.ch)) {
				sc.SetState(SCE_H_OTHER);
				continue;
			}
			break;

		case SCE_H_VALUE:
		case SCE_H_SGML_1ST_PARAM:
			if (IsHtmlInvalidAttrChar(sc.ch)) {
				const int outer = (sc.state == SCE_H_VALUE) ? SCE_H_OTHER : SCE_H_SGML_DEFAULT;
				sc.SetState(outer);
				continue;
			}
			break;

		case SCE_H_SINGLESTRING:
		case SCE_H_SGML_SIMPLESTRING:
			if (sc.ch == '\'') {
				const int outer = (sc.state == SCE_H_SINGLESTRING) ? SCE_H_OTHER : SCE_H_SGML_DEFAULT;
				sc.ForwardSetState(outer);
				continue;
			}
			lexer.DetectAutoLink();
			break;

		case SCE_H_DOUBLESTRING:
		case SCE_H_SGML_DOUBLESTRING:
			if (sc.ch == '\"') {
				const int outer = (sc.state == SCE_H_DOUBLESTRING) ? SCE_H_OTHER : SCE_H_SGML_DEFAULT;
				sc.ForwardSetState(outer);
				continue;
			}
			lexer.DetectAutoLink();
			break;

		case SCE_H_OTHER:
			if (sc.ch == '>' || sc.Match('/', '>')) {
				lexer.tagState = HtmlTagState::None;
				sc.SetState(SCE_H_TAG);
				sc.Forward((sc.ch == '/') ? 2 : 1);
				sc.SetState(lexer.TryTakeOuterStyle());
				break;
			}
			if (sc.ch == '<' || (visibleBefore == 0 && (sc.ch == '#' || sc.ch == '*'))) {
				// html tag on typing, TODO: check other block start characters
				lexer.tagState = HtmlTagState::None;
				sc.SetState(SCE_MARKDOWN_DEFAULT);
				break;
			}
			if (sc.ch == '\'') {
				sc.SetState(SCE_H_SINGLESTRING);
			} else if (sc.ch == '\"') {
				sc.SetState(SCE_H_DOUBLESTRING);
			} else if (IsHtmlAttrStart(sc.ch)) {
				sc.SetState(SCE_H_ATTRIBUTE);
			} else if (!IsHtmlInvalidAttrChar(sc.ch)) {
				sc.SetState(SCE_H_VALUE);
			}
			break;

		case SCE_H_COMMENT:
			if (sc.Match('-', '-')) {
				do {
					sc.Forward();
				} while (sc.ch == '-');
				// close HTML comment with --!>
				// https://html.spec.whatwg.org/multipage/parsing.html#parse-error-incorrectly-closed-comment
				if (sc.ch == '>' || sc.Match('!', '>')) {
					lexer.tagState = HtmlTagState::None;
					sc.Forward((sc.ch == '>') ? 1 : 2);
					sc.SetState(lexer.TryTakeOuterStyle());
					continue;
				}
			}
			lexer.DetectAutoLink();
			break;

		case SCE_H_CDATA:
			if (sc.Match(']', ']', '>')) {
				const HtmlTagState tag = lexer.tagState;
				lexer.tagState = HtmlTagState::None;
				sc.Forward(3);
				if (tag == HtmlTagState::None && sc.GetLineNextChar() == '\0') {
					lineState |= LineStateBlockEndLine;
				} else {
					sc.SetState(lexer.TryTakeOuterStyle());
					continue;
				}
			} else {
				lexer.DetectAutoLink();
			}
			break;

		case SCE_H_QUESTION:
		case SCE_H_PROCESS_TEXT:
			if (sc.Match('?', '>')) {
				if (sc.state == SCE_H_PROCESS_TEXT) {
					sc.SetState(SCE_H_QUESTION);
				}
				lexer.tagState = HtmlTagState::None;
				sc.Forward(2);
				sc.SetState(lexer.TryTakeOuterStyle());
				continue;
			}
			if (sc.state == SCE_H_QUESTION) {
				if (IsASpace(sc.ch)) {
					sc.SetState(SCE_H_PROCESS_TEXT);
				}
			} else {
				lexer.DetectAutoLink();
			}
			break;

		case SCE_H_SGML_COMMENT:
			if (sc.Match('-', '-')) {
				sc.Forward();
				sc.ForwardSetState(SCE_H_SGML_DEFAULT);
				continue;
			}
			break;

		case SCE_H_SGML_COMMAND:
		case SCE_H_SGML_DEFAULT:
			if (sc.ch == '>') {
				if (sc.state == SCE_H_SGML_DEFAULT) {
					sc.SetState(SCE_H_SGML_COMMAND);
				}
				lexer.tagState = HtmlTagState::None;
				sc.ForwardSetState(lexer.TryTakeOuterStyle());
				continue;
			}
			if (sc.state == SCE_H_SGML_COMMAND) {
				if (IsASpace(sc.ch)) {
					sc.SetState(SCE_H_SGML_DEFAULT);
				}
			} else {
				if (sc.ch == '\"') {
					sc.SetState(SCE_H_SGML_DOUBLESTRING);
				} else if (sc.ch == '\'') {
					sc.SetState(SCE_H_SGML_SIMPLESTRING);
				} else if (sc.Match('-', '-')) {
					sc.SetState(SCE_H_SGML_COMMENT);
					sc.Forward();
				} else if (IsASpace(sc.chPrev)) {
					if (IsUpperCase(sc.ch) || (sc.ch == '#' && IsUpperCase(sc.chNext))) {
						sc.SetState(SCE_H_SGML_COMMAND);
					} else if (!IsHtmlInvalidAttrChar(sc.ch)) {
						sc.SetState(SCE_H_SGML_1ST_PARAM);
					}
				}
			}
			break;
		}

		if (sc.state == SCE_MARKDOWN_DEFAULT) {
			if (sc.ch > ' ') {
				if (visibleBefore == 0) {
					if (indentCurrent != indentPrevious) {
						lexer.UpdateParentIndentCount(indentCurrent);
					}
					const int indentCount = indentCurrent - lexer.indentParent;
					if (indentCount < 4) {
						headerLevel = lexer.HighlightBlockText(lineState);
						if (headerLevel == SCE_MARKDOWN_SETEXT_H1 || headerLevel == SCE_MARKDOWN_SETEXT_H2) {
							lineState |= LineStateSetextFirstLine;
						}
						if (headerLevel != 0 && lexer.indentParent != 0) {
							headerLevel = 0; // avoid code folding inside container
						}
					} else {
						lineState = lexer.HighlightIndentedText(lineState, indentCount);
					}
				}
				if (sc.state == SCE_MARKDOWN_DEFAULT) {
					lexer.HighlightInlineText();
				}
			}
		}

		if (sc.atLineEnd) {
			if (fold) {
				int nextLevel;
				if (headerLevel) {
					headerLevel -= (headerLevel <= SCE_MARKDOWN_HEADER6) ? SCE_MARKDOWN_HEADER1 : SCE_MARKDOWN_SETEXT_H1;
					headerLevel += SC_FOLDLEVELBASE;
					nextLevel = headerLevel | SC_FOLDLEVELHEADERFLAG;
					if (nextLevel == prevLevel || (prevLevel != SC_FOLDLEVELBASE && (lineState & LineStateEmptyLine) != 0)) {
						styler.SetLevel(sc.currentLine - 1, headerLevel);
					}
				} else if (prevLevel & SC_FOLDLEVELHEADERFLAG) {
					nextLevel = (prevLevel & SC_FOLDLEVELNUMBERMASK) + 1;
				} else {
					nextLevel = prevLevel;
				}
				styler.SetLevel(sc.currentLine, nextLevel);
				prevLevel = nextLevel;
			}
			if (!lexer.nestedState.empty()) {
				const int outer = lexer.nestedState.front();
				if (IsHeaderStyle(outer)) {
					lexer.tagState = HtmlTagState::None;
					lexer.nestedState.clear();
					lexer.backPos.clear();
					sc.SetState(outer);
				}
			}

			lineState = (lineState & LineStateBlockEndLine)
				| static_cast<int>(lexer.tagState)
				| (lexer.delimiterCount << 8);
			if (visibleChars == 0) {
				lineState |= LineStateEmptyLine;
			} else {
				lineState |= (indentCurrent << 16);
				indentPrevious = indentCurrent;
			}
			if (indentChild != 0) {
				lineState |= LineStateListItemFirstLine | (static_cast<uint32_t>(indentChild) << 24);
			}
			if (lexer.tagState != HtmlTagState::None || StyleNeedsBacktrack(sc.state) || !lexer.nestedState.empty()) {
				lineState |= LineStateNestedStateLine;
			}
			styler.SetLineState(sc.currentLine, static_cast<int>(lineState));
		}
		sc.Forward();
	}

	sc.Complete();
}

}

extern const LexerModule lmMarkdown(SCLEX_MARKDOWN, ColouriseMarkdownDoc, "markdown");
